package com.gsbelarus.gedemin.lib.sync.protocol;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;

import com.gsbelarus.gedemin.lib.sync.protocol.entity.SyncProperty;
import com.gsbelarus.gedemin.lib.sync.protocol.etc.DownloadHelper;
import com.gsbelarus.gedemin.lib.sync.protocol.etc.Parser;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.AddressFailedException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.CustomSyncException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.NotRequiredException;
import com.gsbelarus.gedemin.lib.sync.protocol.exception.StopSyncException;

import java.io.IOException;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.List;

final public class SyncProtocol {

    private Context context;

    /**
     * Статус завершения синхронизации
     * (NO_INTERNET_CONNECTION, ADDRESS_FAILED, TIMEOUT,
     * NOT_REQUIRED, ELSE_FAILED, SUCCESSFUL, STOP_SYNC, CUSTOM_SYNC_FAILED)
     */
    public enum SyncStatus {

        /**
         * Нет подключения к интернет
         */
        NO_INTERNET_CONNECTION,

        /**
         * Ошибка адреса сервера, возникает если пришли "левые" данные
         */
        ADDRESS_FAILED,

        /**
         * Таймаут (30 сек)
         */
        TIMEOUT,

        /**
         * Обновление не требуется, возникает если макс версия равна текущей
         */
        NOT_REQUIRED,

        /**
         * Другие ошибки
         */
        ELSE_FAILED,

        /**
         * Синхронизация завершена успешно
         */
        SUCCESSFUL,

        /**
         * Синхронизация остановлена
         */
        STOP_SYNC,

        /**
         * Пользовательская ошибка, можно сгенерировать в момент анализа данных
         */
        CUSTOM_SYNC_FAILED;

        private String message = "";

        public String getMessage() {
            return message;
        }

        public SyncStatus setMessage(String message) {
            this.message = message;
            return this;
        }
    }

    private boolean isWork = false;
    private volatile boolean isStopSync = false;

    private SyncProtocolModel listener;

    /**
     * Констуктор протокола, запуск синхронизации производится методом doSync
     *
     * @param context  контекст приложения
     * @param listener интерфейс взаимодействия между приложением и протоколом синхронизации
     */
    public SyncProtocol(Context context, SyncProtocolModel listener) {
        this.listener = listener;
        this.context = context;
    }

    /**
     * Провести синхронизацию.
     *
     * @return true - синхронизация запущена, false - синхронизация уже работает
     */
    public boolean doSync(SyncProperty syncProperty) {
        if (isWork)
            return false;
        else
            new Sync().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, syncProperty);

        return true;
    }

    /**
     * Проверка на наличие интернет соединения
     *
     * @return есть соединение или нет
     */
    public boolean checkInternetConnection() {
        ConnectivityManager conMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo netInfo = conMgr.getActiveNetworkInfo();
        return !(netInfo == null || !netInfo.isConnected());
    }

    /**
     * Класс загрузки и обновления БД в отдельном потоке
     */
    private class Sync extends AsyncTask<SyncProperty, Integer, SyncStatus> implements DownloadHelper.OnChangeProgress {

        private int totalBlocks = 1;            // общее количество блоков
        private int currentBlock = 1;           // загружаемый блок

        private int bufBlockPercent = 0;        // буферная переменная для состояния загрузки одного блока
        private float globalPercent = 0;        // переменная для общего состояния загрузки
        private float bufGlobalPercent = 0;     // буферная переменная для общего состояния загрузки

        /**
         * расчет общего прогресса. blockPercent - прогресс загрузки одного блока
         */
        @Override
        public void onChangeProgress(int blockPercent) {

            /** если буферная переменная стостояния загрузки одного блока больше текущей загрузки,
             *значит блок докачался и можно обнулять буферную переменную */
            if (blockPercent < bufBlockPercent)
                bufBlockPercent = 0;

            /** расчет общего состояния загрузки */
            globalPercent += 100 / (float) totalBlocks / 100 * (float) (blockPercent - bufBlockPercent);

            bufBlockPercent = blockPercent;

            /** публиковать прогресс только если изменения общего состояния загрузки больше чем на 5% */
            if (globalPercent - bufGlobalPercent >= 5) {
                publishProgress((int) globalPercent, currentBlock, totalBlocks);
                bufGlobalPercent = globalPercent;
            }
        }

        /**
         * Выполняется во время подготовки к созданию нового потока загрузки, имеет доступ к UI потоку
         */
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            isStopSync = false;
            isWork = true;
            listener.onPreExecute();
            /** Проверка на подключение к сети */
            if (!checkInternetConnection()) {
                cancel(true);
                onPostExecute(SyncStatus.NO_INTERNET_CONNECTION);
            }
        }

        @Override
        protected SyncStatus doInBackground(SyncProperty... voids) {
            SyncProperty property = voids[0];
            SyncStatus status;

            status = SyncStatus.SUCCESSFUL.setMessage("");
            DownloadHelper downloadHelper = new DownloadHelper(this);
            listener.onBeginTransaction();

            try {
                Parser parser = new Parser();

                checkIsStopped();

                int maxNum;
                int curNum;
                if (property.getType() == SyncProperty.Type.BLOCKS) {
                    /** загрузка блоками*/

                    /** делаем запрос на получение макс версии*/
                    List<String> maxVer = downloadHelper.download(property.getUrlForMaxVer(), false);

                    /** анализируем заголовок, получаем макс версию*/
                    HashMap<String, String> headerMap = parser.parsingHeader(maxVer);
                    listener.onHeaderLoaded(headerMap);
                    if (!headerMap.containsKey(Parser.BLOCK_NUM)) {
                        listener.onEndTransaction();
                        return status;
                    }
                    maxNum = Integer.valueOf(headerMap.get(Parser.BLOCK_NUM));
                    curNum = property.getCurrentVerData();
                } else {
                    /** загрузка одним блоком*/
                    maxNum = 1;
                    curNum = 0;
                }

                /** запускаем цикл запросов на сервер*/
                if (maxNum > curNum) {
                    totalBlocks = maxNum - curNum;
                    if (property.getType() == SyncProperty.Type.BLOCKS)
                        publishProgress(0, currentBlock, totalBlocks);
                    for (int i = curNum + 1; i <= maxNum; i++) {
                        List<String> lines = null;
                        /** запуск цикла для повторения запросов в случае timeout exception */
                        for (int count = 0; count < property.getCountConnections(); count++) {
                            try {
                                checkIsStopped();

                                lines = downloadHelper.download(property.getUrlStringForVer(i), true);
                                break;
                            } catch (SocketTimeoutException e) {
                                if (count == property.getCountConnections() - 1)
                                    throw new SocketTimeoutException();
                                Thread.sleep(property.getDelayBetweenReconnect());
                            }
                        }
                        currentBlock++;

                        HashMap<String, String> headerMapBlock = parser.parsingHeader(lines);
                        listener.onHeaderLoaded(headerMapBlock);

                        parser.removeHeaderFromList(lines);

                        if (property.getType() == SyncProperty.Type.ONE_BLOCK)
                            publishProgress(101, currentBlock, totalBlocks);
                        listener.onBlockLoaded(lines);
                    }

                    listener.onEndTransaction();

                } else
                    throw new NotRequiredException();

            } catch (CustomSyncException e) {
                status = SyncStatus.CUSTOM_SYNC_FAILED.setMessage(e.getMessage());

            } catch (AddressFailedException e) {
                status = SyncStatus.ADDRESS_FAILED.setMessage(e.getMessage());

            } catch (SocketTimeoutException e) {
                status = SyncStatus.TIMEOUT.setMessage("");

            } catch (NotRequiredException e) {
                status = SyncStatus.NOT_REQUIRED.setMessage("");

            } catch (StopSyncException e) {
                status = SyncStatus.STOP_SYNC.setMessage("");

            } catch (IndexOutOfBoundsException e) {
                /** может произойти если во время синхронизации сервер выключился или прислал пустой ответ.
                 * генерируется в классе Parser */
                status = SyncStatus.ELSE_FAILED.setMessage(e.getMessage());

            } catch (IllegalArgumentException e) {
                status = SyncStatus.ADDRESS_FAILED.setMessage(e.getMessage());

            } catch (IOException e) {
                /** во всех других случаях говорим о сбое*/
                status = SyncStatus.ELSE_FAILED.setMessage(e.getMessage());

            } catch (InterruptedException e) {
                status = SyncStatus.ELSE_FAILED.setMessage(e.getMessage());
            }

            if (status != SyncStatus.SUCCESSFUL)
                listener.onCancelTransaction();

            return status;
        }

        /**
         * Выполняется после загрузки, имеет доступ к UI потоку
         */
        @Override
        protected void onPostExecute(SyncStatus status) {
            super.onPostExecute(status);
            isWork = false;

            if (isStopSync)
                status = SyncStatus.STOP_SYNC;

            /** обрабатываем ошибки */
            listener.onPostExecute(status);
        }

        /**
         * Обновление состояния загрузки
         */
        @Override
        protected void onProgressUpdate(Integer... values) {
            listener.onProgressUpdate(values[0], values[1], values[2]);
        }
    }

    /**
     * проверка, остановлена синхронизация или нет
     */
    private void checkIsStopped() throws StopSyncException {
        if (isStopSync)
            throw new StopSyncException();
    }

    /**
     * @return работает ли синхронизация или создание демо данных
     */
    public boolean isWork() {
        return isWork;
    }

    /**
     * Прервать синхронизацию
     */
    public void setStopSync(boolean flag) {
        isStopSync = flag;
    }

    public boolean isStopSync() {
        return isStopSync;
    }
}
